我的全域 CLAUDE.md 曾經有 71 行。裡面把 iOS 開發的每個階段都寫得很細:Phase 0 要做什麼、Phase 1 要做什麼、每個 skill 什麼時候用。我很滿意,覺得 AI 一定會照著做。
它沒有。不是不讀,是讀了但不照做——因為每次對話都要吞這 71 行,重點被稀釋在細節裡。我後來把它瘦成 43 行:只留階段總覽和規模判斷表,細節搬到另一份 playbook,用絕對路徑指過去。AI 的行為反而變穩了。
原因很簡單:CLAUDE.md 是每次都載入的東西,它的成本是「每一句話都佔注意力」。 長了,AI 讀完不記得重點;短了,它每次都知道該去哪找。
Claude Code 有三個地方可以「教」它東西。新人最常犯的錯是全部塞進 CLAUDE.md。
| CLAUDE.md | Skill | Memory | |
|---|---|---|---|
| 是什麼 | 規則 | SOP | 事實 |
| 什麼時候載入 | 每次對話都載入 | 被觸發時才載入 | 相關時才回想 |
| 該多長 | 短(<100 行) | 可以長 | 一則一個事實 |
| 詞性 | 「永遠這樣做」 | 「遇到 X 時,按 Y 步驟做」 | 「這個專案的 Z 是這樣」 |
| 例子 | 「改邏輯先寫測試」 | 「除錯:收集症狀→三個假設→驗證→才改 code」 | 「這台機器的 simulator 叫 iPhone 16」 |
一個好記的分法:CLAUDE.md 是憲法,Skill 是 SOP 手冊,Memory 是便利貼。 憲法要短、要每個人都背得出來;SOP 可以厚,但只在做那件事時翻;便利貼隨手貼、隨時撕。
今天只做憲法。Skill 是 Day 5。
不是 README。README 是給人看的「這專案是什麼」;CLAUDE.md 是給 AI 看的「在這專案裡工作,什麼不能做」。
五段,順序有意義:
沒有的東西:「請寫出高品質的 code」、「遵循最佳實踐」、「注意效能」。這些是空話,AI 讀了不會做任何不同的事。一條規則如果不能被違反,就不是規則。
用系列的載體專案示範。iPlaygroundIMS 是一個研討會工作人員任務 app:選名字看當下任務、任務前 10 分鐘本地通知、主畫面 Widget、Live Activity 倒數。iOS 17、SwiftUI、xcodegen、無第三方套件、38 個 commit。它到今天為止沒有 CLAUDE.md——正好。
第一步:讓 AI 先告訴你它以為的專案長什麼樣。 這步很多人跳過,但它是最有價值的一步——你會看到 AI 誤解了什麼,那些誤解就是 CLAUDE.md 該寫的東西。
讀這個專案,不要改任何東西。給我:
1. 一句話說這個 app 做什麼
2. 你認為的架構重點(三到五條)
3. build 和 test 的指令
4. 你認為有哪些「不能亂動」的設計決策
5. 你不確定的地方
在 IMS 上跑這段,它會答對大部分,但通常會漏兩件事:JavaScriptCore 為什麼不能進 widget(記憶體限制,widget extension 會被殺)、「選名字」和「開啟提醒」為什麼是分開的兩個動作。這兩件事在 README 裡有寫,但它是「設計決策」,不是「程式結構」——AI 讀 code 讀不出來為什麼。
第二步:寫。 下面是我為 IMS 寫的版本,49 行:
# IMS · iPlayground Mission System
iOS 17+ SwiftUI app。研討會工作人員選自己的名字 → 看當下/下一場任務,
用本地通知、Widget、Live Activity 提醒。純本地、無自建後端。
## 指令
- 產 xcodeproj:`xcodegen generate`(target 設定改 `project.yml`,不要直接改 xcodeproj)
- 測試:`xcodebuild test -scheme IMS -destination 'platform=iOS Simulator,name=iPhone 16'`
- Live Activity/本地通知**只能真機測**,模擬器不 render Live Activity
## 架構不變量(要改先問我)
- JavaScriptCore 只在 App target 執行,**絕不進 IMSWidget**(widget extension 有記憶體上限)。
App 解析後寫 JSON 快照到 App Group,Widget 純原生讀快照。
- 資料來源順序:遠端 GitHub Pages → App Group cache → bundle 內建快照。三層 fallback 缺一不可。
- `activatedPerson` 是通知/Widget/Live Activity 的唯一真相。
「選名字」只是檢視,不啟動任何提醒;只有「這是我・開啟提醒」開關才寫入 activatedPerson。
- 任務以**合併後的 block** 為單位(同日同 role 相連時段合併),通知也以 block 為單位——
這是為了 iOS 64 則本地通知上限。
- `Shared/` 同時編進 app 與 widget,不得依賴 app-only 的東西。
## 目錄
- `IMS/` app 本體。`Services/` 抓資料、解析、排通知、Live Activity;`Views/` UI
- `IMSWidget/` Widget 與 Live Activity 的 UI
- `Shared/` 兩邊共用的 model 與純函式(BlockBuilder、MissionTimeline、RosterBuilder)
- `IMSTests/` 單元測試。`Fixtures/` 是測試用資料快照(假名),**不要用真實資料覆蓋**
## 工作方式
- 改邏輯先寫會失敗的測試;能寫成純函式的邏輯放 `Shared/`,讓它可測
- 沒跑過測試不准說「完成」,貼測試輸出
- 不要為了讓測試通過改測試
- 一次一個 task;一次改動超過 8 個檔案先停下來,跟我討論怎麼拆
- 回覆用繁體中文,術語和程式碼保持英文
## Debug 旗標(Scheme → Environment Variables)
`IMS_TEST_TODAY=1`(把 D0 當今天並注入測試任務)、`IMS_PRESELECT=<名字>`、
`IMS_ACTIVATE=1`、`IMS_PREVIEW_BANNER=1`、`IMS_TEST_LIVEACTIVITY=1`
注意「架構不變量」那段每一條都是「絕不」「唯一」「缺一不可」——都是可以被違反的規則,所以 AI 違反時你抓得到。
第三步:驗證。 開一個新的對話(舊對話的 context 會汙染結果),問三個問題:
1. 我想讓 Widget 直接抓遠端資料,不經過 App,可以嗎?
2. 使用者選了名字之後,通知會自動排嗎?
3. 跑一下測試,告訴我結果。
第一題它應該說不行並講出原因;第二題應該說不會、要按開關;第三題應該真的跑 xcodebuild test 而不是說「我無法執行」。三題有一題答錯,回去改 CLAUDE.md 對應那段。
把 README 貼進去。 README 講「是什麼」,CLAUDE.md 講「不能做什麼」。AI 讀 code 就能知道是什麼,它需要的是 code 裡讀不出來的「為什麼」。
寫空話。 「保持 code 乾淨」「遵循 SOLID」。問自己:這條規則 AI 有可能違反嗎?如果不可能被違反,它就不是規則,刪掉。
寫超過 100 行。 你以為寫越多 AI 越懂,其實是每一條的權重都被稀釋。超過 100 行代表你該把某些段落搬去 Skill(Day 5)或另一份文件,用路徑指過去。
把一次性任務寫進去。 「這次要把 X 改成 Y」——這是 prompt,不是憲法。做完就過期,但它會留在每次對話裡。
沒驗證就當它有效。 寫完不開新對話測一次,你不知道它到底有沒有讀進去。三題驗證法花五分鐘。
CLAUDE.md.template——五段骨架,每段有提示。上面的 IMS 版本是填好的實例。
# <專案名>
<一句話:這是什麼 app、給誰用、最重要的架構特徵(例如:純本地無後端)>
## 指令
- build:
- test:
- <特殊限制:例如某功能只能真機測>
## 架構不變量(要改先問我)
- <用「絕不」「唯一」「一律」開頭;每條都要是「可以被違反」的規則>
-
-
## 目錄
- `<資料夾>/` <放什麼>
-
## 工作方式
- 改邏輯先寫會失敗的測試
- 沒跑過測試不准說「完成」,貼測試輸出
- 不要為了讓測試通過改測試
- 一次改動超過 8 個檔案先停下來討論怎麼拆
- <語言/風格>
寫完做一件事:數行數。超過 100 行,回頭找哪一段其實是 SOP。